Skip to content

fix(run_delivery): deliver triggered run failures to the creator (#6896) - #7131

Merged
serrrfirat merged 15 commits into
mainfrom
implement-issue-6896-fix
Aug 10, 2026
Merged

serrrfirat merged 15 commits into
mainfrom
implement-issue-6896-fix

Conversation

@serrrfirat

@serrrfirat serrrfirat commented Aug 4, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • Triggered runs ending in Failed, RecoveryRequired, or Cancelled now deliver sanitized terminal notices to the creator's configured preference target instead of being recorded silently as Skipped.
  • Runs that exceed the actionable-state wait now deliver a timeout notice and preserve resolution-specific delivery outcomes.
  • Failed accepted trigger fires emit a required settlement callback only after durable active-fire cleanup; the production composition records structured health telemetry without minting a replacement run.
  • Added caller-level delivery, lost-clear-race, and production-observer regressions and documented the settlement contract.

Change Type

  • Bug fix
  • New feature
  • Refactor
  • Documentation
  • CI/Infrastructure
  • Security
  • Dependencies

Linked Issue

Fixes #6896

Validation

  • cargo fmt --all -- --check
  • cargo clippy -p ironclaw_product -p ironclaw_triggers -p ironclaw_reborn_composition --all-targets --all-features -- -D warnings
  • cargo build (covered by clippy and test builds)
  • Relevant tests pass: full run-delivery contract, full trigger suite and repository contracts, production observer test, and architecture boundary test
  • cargo test --features integration if database-backed or integration behavior changed — Not applicable: no schema/backend contract changed; focused composition coverage exercises the production observer
  • Manual testing — Not run; deterministic caller-level and production-wrapper tests cover this behavior
  • Multi-agent code review completed: fix(run_delivery): deliver triggered run failures to the creator (#6896) #7131 (review)

Test Strategy

User behavior:

A scheduled run that fails, requires recovery, is cancelled, or times out now sends one safe terminal notice to the creator's configured target instead of disappearing silently.

Risk areas:

  • Model behavior
  • Browser
  • Side effect
  • Persistence
  • Security or permissions
  • External provider
  • Cross-component behavior

Tests added or updated:

  • Unit or contract: failed, recovery-required, uncategorized cancellation, categorized cancellation, timeout delivery, and missing-target timeout outcome tests through on_trigger_submitted.
  • Reborn integration: focused production composition observer test verifies structured failure health telemetry and no delivery bypass.
  • Recorded fixture: Not applicable: no provider payload or wire parsing changed.
  • Browser E2E: Not applicable: no browser/WebUI behavior changed.
  • Backend or runtime: full ironclaw_triggers suite, including successful failure settlement, successful terminal non-emission, and lost-clear-race non-emission.
  • Live canary: Not applicable: behavior is deterministic and needs no live credential/provider.

What the tests prove:

  • Each new terminal branch reaches the decoded creator preference target with a sanitized summary and triggered footer.
  • A timeout without a configured target records NoDefaultConfigured and sends nothing.
  • Failed settlement fires only after durable clear and never after a lost clear race.
  • The production observer emits the automation-health warning without invoking the delivery hook.
  • Dependency boundaries remain intact.

Commands run:

cargo test -p ironclaw_product --test run_delivery_contract
cargo test -p ironclaw_triggers
cargo test -p ironclaw_reborn_composition --lib failed_settlement_emits_health_warning_without_delivery -- --nocapture
cargo test -p ironclaw_architecture reborn_crate_dependency_boundaries_hold
cargo clippy -p ironclaw_product -p ironclaw_triggers -p ironclaw_reborn_composition --all-targets --all-features -- -D warnings
cargo fmt --all -- --check
git diff --check

Security Impact

None. Outbound failure text comes only from the existing static sanitized-summary table. Raw provider/runtime failures are never delivered. The settlement callback carries typed identities only and cannot mint runs or mutate trigger state.

Reborn Trust-Boundary Checklist

  • Public policy/evidence/trust-bearing types: TriggerFailedFireSettlement is constructed by active cleanup only after the exact fire is durably cleared.
  • Untrusted content enters prompts only through an envelope/escaping primitive: no new prompt input; notification text is static and sanitized.
  • Hashes declare purpose: Not applicable; no hashes changed.
  • New/changed status, exit, policy, runtime, or error variants: no variants added; terminal status match sites and all settlement-observer implementations were enumerated.
  • Security/durability serde(default) fields fail closed: Not applicable; no serialized fields changed.
  • Queues/maps/buffers/counters are bounded: no new collection or queue.
  • Driver/operator-visible errors retain stable semantics: delivery resolution outcomes remain typed and the observer adds structured health telemetry only.
  • Sandbox/native/host names accurately describe trust boundary: Not applicable; no runtime lane changed.

Database Impact

None. No migration or persisted schema changes. Existing TriggerRunHistoryStatus::Error history remains authoritative.

Blast Radius

Touches triggered-run outbound delivery, trigger active-fire cleanup observation, composition health telemetry, and their tests. The observer method is required so all production, no-op, and test implementations remain explicit.

Rollback Plan

Revert the PR. No schema, configuration, or persisted-format migration needs rollback.

Review Follow-Through

All currently accessible CodeRabbit and multi-agent review findings are addressed in commit 8ab564656. Retry/redrive policy remains intentionally out of scope and does not bypass canonical delivery.


Review track: B

Scheduled/triggered runs that ended in Failed, Cancelled, or
RecoveryRequired produced no user-visible notification: the triggered
delivery driver minted notifications only for Completed /
BlockedApproval / BlockedAuth and recorded every other terminal status
as Skipped. A run that timed out before reaching an actionable state
only logged a warn and recorded Failed, leaving the creator in silence.

Delivery:
- triggered_notification_for_state now mints a FinalReplyReady
  notification for Failed and RecoveryRequired using the existing
  per-category failure summaries (reborn_failure_summary_for_category)
  over state.failure.category(), with a generic fallback when no
  category is present.
- Cancelled mints the same notification, preferring a failure-category
  summary when one is present and falling back to a fixed cancellation
  notice otherwise.
- The RunWaitTimedOut branch with no prior blocked marker now delivers
  the timeout notice as a terminal reply instead of recording Failed.
- The wildcard arm is replaced with explicit non-actionable statuses
  (Queued, Running, CancelRequested, BlockedResource,
  BlockedDependentRun, BlockedExternalTool) so a future status fails to
  compile rather than silently skipping.

Observer:
- TriggerFireSettlementObserver gains on_failed_fire_settled as a
  default no-op method, plus a TriggerFailedFireSettlement event
  carrying tenant/trigger/fire-slot/run-id/history-status. Noop and
  existing implementors keep compiling.
- The active-cleanup sweep fires on_failed_fire_settled when
  clear_active_fire succeeds with TriggerRunHistoryStatus::Error, so
  post-accept failures are observable for automation health. Ok,
  Running, and already-cleared fires do not fire the hook.

Tests:
- run_delivery_contract: Failed+model_error, Failed without category,
  Cancelled, and timeout-before-actionable all assert a Delivered
  outcome with the expected notice text and footer.
- worker tests: a terminal-Error active fire fires exactly one
  on_failed_fire_settled; a terminal-Ok active fire fires none.

The larger retry/redrive budget for failed post-accept fires
(retry_disposition has zero production callers) is intentionally left
for a follow-up; it is out of scope for this surgical delivery fix.
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@railway-app

railway-app Bot commented Aug 4, 2026 •

Copy link
Copy Markdown

🚅 Deployed to the pr-a7ad01-7131 environment in ironclaw-ci-preview

Service Status Web Updated (UTC)
ironclaw ✅ Success (View Logs) Web Aug 10, 2026 at 12:43 pm

@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-7131 August 4, 2026 10:08 Destroyed
@coderabbitai

coderabbitai Bot commented Aug 4, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@serrrfirat, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 1 minute

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: cdf11d21-70e7-4940-9417-6c229a9f9d43

📥 Commits

Reviewing files that changed from the base of the PR and between b1ed4ec and 767f95d.

📒 Files selected for processing (6)
  • crates/app/ironclaw_architecture_tests/tests/reborn_restructure_baselines.rs
  • crates/app/ironclaw_composition/Cargo.toml
  • crates/app/ironclaw_composition/src/automation/trigger_poller.rs
  • crates/product/ironclaw_assistant/tests/run_delivery_contract.rs
  • docs/reborn/contracts/triggers.md
  • scripts/ci/composition-budget.toml
📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added notifications for triggered runs that fail, require recovery, are canceled, or time out.
    • Failure notices include categorized summaries when available, with clear cancellation messaging.
    • Terminal run outcomes now provide clearer delivery status and automation-health warnings.
  • Bug Fixes

    • Improved handling of terminal-state timing races so final notices are delivered when possible.
    • Prevented duplicate failure notifications during cleanup races.
    • Avoided post-submit delivery for failed trigger settlements.

Walkthrough

Triggered runs now deliver terminal failure, recovery-required, cancellation, and timeout notices. Active trigger cleanup emits failed-run settlement events after newly clearing terminal errors without invoking post-submit delivery.

Changes

Triggered failure delivery

Layer / File(s) Summary
Terminal triggered-run notifications
crates/product/ironclaw_assistant/src/run_delivery/triggered.rs, crates/product/ironclaw_assistant/src/run_delivery/prompts.rs
Triggered delivery handles categorized failures, recovery-required states, cancellations, and timeouts. Shared fan-out logic records delivery outcomes.
Delivery state fixtures and regression coverage
crates/product/ironclaw_assistant/tests/run_delivery_contract.rs
Scripted states support failure categories and late terminal transitions. Tests cover terminal notices, timeout handling, delivery failures, and cancellation precedence.

Trigger settlement reporting

Layer / File(s) Summary
Failed active-fire settlement reporting
crates/domains/ironclaw_triggers/src/worker/ports.rs, crates/domains/ironclaw_triggers/src/worker/active_cleanup.rs, crates/domains/ironclaw_triggers/src/worker/tests.rs
The worker exposes TriggerRunFailureSettlement. Cleanup reports newly cleared terminal errors. Tests cover successful cleanup and clear races.
Settlement integration and contract documentation
crates/app/ironclaw_composition/src/automation/trigger_poller.rs, crates/domains/ironclaw_triggers/src/lib.rs, crates/domains/ironclaw_triggers/src/worker.rs, docs/reborn/contracts/triggers.md
The poller logs failed settlements without invoking post-submit delivery. Public exports and the trigger contract document the observer behavior.

Composition budget

Layer / File(s) Summary
Production LOC baseline
crates/app/ironclaw_architecture_tests/tests/reborn_restructure_baselines.rs, scripts/ci/composition-budget.toml
The recorded composition LOC baseline and ceiling increase to 41,337.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant TriggeredRunDelivery
  participant TurnRunState
  participant PreferenceTarget
  TriggeredRunDelivery->>TurnRunState: wait for terminal or timeout state
  TurnRunState-->>TriggeredRunDelivery: failure, cancellation, or timeout
  TriggeredRunDelivery->>PreferenceTarget: deliver terminal notice
Loading
sequenceDiagram
  participant ActiveCleanup
  participant TriggerFireSettlementObserver
  participant TriggerPoller
  ActiveCleanup->>TriggerFireSettlementObserver: report failed settlement identifiers
  TriggerFireSettlementObserver->>TriggerPoller: log failed settlement warning
  TriggerPoller-->>ActiveCleanup: do not invoke post-submit delivery
Loading

Possibly related PRs

Suggested reviewers: benkurrek

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR re-seeds composition LOC baselines with unrelated #7171 and intervening-main changes that are not required by #6896. Move unrelated LOC baseline re-seeding to a separate maintenance PR, or limit the baseline update to lines introduced by this change.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title uses Conventional Commits syntax and accurately describes the primary triggered-run failure delivery change.
Description check ✅ Passed The description follows the repository template and documents scope, validation, tests, security, database impact, rollback, and review follow-through.
Linked Issues check ✅ Passed The changes satisfy #6896 by delivering terminal failure notices, handling timeout outcomes, and exposing post-cleanup failure settlement telemetry.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added size: L 200-499 changed lines risk: low Changes to docs, tests, or low-risk modules contributor: core 20+ merged PRs labels Aug 4, 2026
@ironloopai

ironloopai Bot commented Aug 4, 2026 •

Copy link
Copy Markdown
Contributor

🔎 Review · PR #7131

⚫ Cancelled · Target changed

The target changed before this Run could finish.

Automatic · PR opened + CI failed · attempt 0 of 3 · cancelled after <1s

Run details
  • Repository: nearai/ironclaw
  • Base: main at 79435f4
  • Head: implement-issue-6896-fix at 9dc9f0c
  • Created: Aug 4, 2026, 10:13 AM UTC
  • Updated: Aug 4, 2026, 10:13 AM UTC
  • Run: 448cf908-2080-4e1b-9956-9f9668107bc4

@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-7131 August 4, 2026 10:55 Destroyed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/ironclaw_product/tests/run_delivery_contract.rs`:
- Around line 2138-2228: Add caller-level tests alongside the existing triggered
delivery tests for RecoveryRequired and Cancelled with Some(SanitizedFailure),
using on_trigger_submitted as the exercised seam. For each branch, assert
Delivered, the branch-specific failure summary, the triggered footer, and
routing to the decoded creator preference target, reusing the existing harness
and assertion patterns.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 2a55030e-f3f7-46e9-bf6b-82488d69a8a4

📥 Commits

Reviewing files that changed from the base of the PR and between 79435f4 and 31765a3.

📒 Files selected for processing (8)
  • crates/ironclaw_product/src/run_delivery/prompts.rs
  • crates/ironclaw_product/src/run_delivery/triggered.rs
  • crates/ironclaw_product/tests/run_delivery_contract.rs
  • crates/ironclaw_triggers/src/lib.rs
  • crates/ironclaw_triggers/src/worker.rs
  • crates/ironclaw_triggers/src/worker/active_cleanup.rs
  • crates/ironclaw_triggers/src/worker/ports.rs
  • crates/ironclaw_triggers/src/worker/tests.rs

Comment thread crates/product/ironclaw_assistant/tests/run_delivery_contract.rs Outdated

@serrrfirat serrrfirat left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review (multi-agent)

Intent: Deliver sanitized creator notifications when triggered or scheduled runs fail, require recovery, are cancelled, or time out.

Shape: normal with no modifiers — 8 files / 477 changed lines, no generated, mechanical, mega, or stacked characteristics.

Coverage: complete via exact local-git diff. Buckets: 7 production files, 1 test file. Packetization not needed. All 8 reviewers completed with no limitations.

Stats: 5 findings from 9 raw findings after overlap dedup, across 3 files. Reviewers run: Security, Bugs, Performance/Concurrency, Tests, Conventions, Local Patterns, Maintainability, Approach. Reviewers failed: none. Body-only: 0.

Conventions / correctness

  1. Medium Default hook silently drops every production failure settlement (crates/ironclaw_triggers/src/worker/ports.rs:146, confidence 100) — anchor: AGENTS.md:186. The production observer inherits the no-op, so the worker's new health event is discarded. Also flagged by Bugs, Local Patterns, Maintainability, and Approach.

Tests

  1. Medium RecoveryRequired terminal delivery has no contract test (crates/ironclaw_product/src/run_delivery/triggered.rs:782-800, confidence 100) — the shared terminal arm is only exercised with Failed.
  2. Low Categorized cancellation summary branch is untested (crates/ironclaw_product/src/run_delivery/triggered.rs:810-814, confidence 100) — only the uncategorized fallback is exercised.
  3. Medium Timeout notice failure mapping is happy-path-only (crates/ironclaw_product/src/run_delivery/triggered.rs:389-403, confidence 100) — no test proves missing-target mapping to NoDefaultConfigured.
  4. Medium Failed-fire observer is not tested against a lost clear race (crates/ironclaw_triggers/src/worker/active_cleanup.rs:169-179, confidence 100) — duplicate suppression after a lost clear race is not pinned.

Security and performance/concurrency found no additional issues.

Comment thread crates/ironclaw_triggers/src/worker/ports.rs Outdated
Comment thread crates/product/ironclaw_assistant/src/run_delivery/triggered.rs Outdated
Comment thread crates/product/ironclaw_assistant/src/run_delivery/triggered.rs Outdated
Comment thread crates/product/ironclaw_assistant/src/run_delivery/triggered.rs Outdated
Comment thread crates/domains/ironclaw_triggers/src/worker/active_cleanup.rs
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-7131 August 4, 2026 12:11 Destroyed
@github-actions github-actions Bot added the scope: docs Documentation label Aug 4, 2026
# Conflicts:
#	crates/product/ironclaw_assistant/src/run_delivery/triggered.rs
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-7131 August 6, 2026 09:35 Destroyed
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@serrrfirat

serrrfirat commented Aug 6, 2026 •

Copy link
Copy Markdown
Collaborator Author

Railway preview QA — BLOCKED (fallback-target journey executed)

Tested head SHA: f6cdf6158e7de5927699f54afddac664bcaabaec (PR head implement-issue-6896-fix)
Railway state: success — https://ironclaw-ironclaw-pr-7131.up.railway.app
Routes exercised: /login, /chat, /automations, /jobs, /logs, /extensions/registry, /settings/inference
Driver: local Chromium (Playwright) against the Railway preview; bearer used only for UI login, never persisted.

Retest with the fallback delivery target

The preview's only available delivery default is the fallback "Web app only (no external delivery)" target (no channel extension is installed or configured). This fallback was verified as the active/selected state on /automations twice (initial load + reload read-back; Save/Clear idle). A real triggered run was then exercised end-to-end against it:

# Classification Case Intended contract Actual contract exercised Result
R1 Required Failed triggered run → terminal notice to creator's preference target Triggered run ending Failed/RecoveryRequired/Cancelled delivers a sanitized terminal notice to the creator's configured outbound target (was silently Skipped) Not executed — preview has no outbound channel: Telegram Available but uninstalled in /extensions/registry; live run log: triggered run delivery skipped: no channel extension owns the delivery … reason=no active channel extension registered a preference codec. Delivered-notice branch untestable BLOCKED
R2 Required Timeout notice / NoDefaultConfigured Run exceeding the actionable-state wait delivers a timeout notice to the configured target; no target → NoDefaultConfigured, sends nothing Not executed as timeout — max wait is 30 min. The adjacent failed-run/no-target branch WAS exercised live: delivery skipped with the logged codec reason, run failure recorded in run history (below). Exact outcome-kind text is not UI-visible BLOCKED
R3 Required Failed trigger fire settlement Failed accepted fire emits the settlement callback only after durable cleanup; no replacement run minted Partially executed live: poller logged accepted trigger fire settled with a failed run … fire_slot=2026-08-06 10:34:00 UTC run_id=528a6442…; exactly one run per fire observed (no duplicate); lost-clear-race variant not browser-observable BLOCKED

Live fallback-target journey (executed through real UI + deployment)

  1. Config read-back: /automations → Delivery Defaults → "Web app only" fallback row selected; no current target; state identical after reload. (Save/Clear not needed — fallback was already the effective default.)
  2. Trigger created via chat: asked the agent for a recurring automation railway-qa-fail-test (*/2 * * * * UTC, trigger ID 01KZBA5978ZV92TXYGBFNYD2ED) whose run calls a nonexistent tool (nonexistent_xyz_tool) to force a terminal failure. Agent confirmed creation with schedule + next-run fields.
  3. Run fired: 10:34:00 UTC fire slot; executor resolved resolved_run_profile_id=scheduled_trigger.
  4. Delivery attempt (fallback target): WARN ironclaw_extension_host::channel_triggered_delivery — triggered run delivery skipped: no channel extension owns the delivery … reason=no active channel extension registered a preference codec. Nothing external was sent — consistent with the "no target configured → sends nothing, records outcome" branch.
  5. Run failed: turn_run_executor … status=Failed (10:35:00).
  6. Settlement: WARN ironclaw_composition::automation::trigger_poller — accepted trigger fire settled with a failed run … fire_slot=2026-08-06 10:34:00 UTC run_id=528a6442-1a3f-4b60-84dc-3bb8f8fb4ffa.
  7. UI recording: automations list → Recent runs: 1, Failed: 1, status NEEDS REVIEW, LAST COMPLETED Aug 6, 01:35 PM, FAILURES counter 1 (danger). Terminal failure is recorded in run history — not silently dropped.
  8. No replacement run: RUNNING NOW returned to 0 after each fire; exactly one run per fire slot; NEXT RUN advanced normally.
  9. Cleanup: test automation deleted via chat (agent confirmed removal; noted "both of its last two runs ended in an error state"); /automations read-back: SCHEDULED 0, ACTIVE 0, FAILURES 0, NEXT RUN None.

Status derivation

  • Required: 0 passed / 0 failed / 3 blocked (not fully executable). Overall heading stays BLOCKED per the mandatory gate: R1's delivered-notice branch and R2's timeout branch require a configured outbound channel the preview does not have; the observed failed-run/no-target behavior is consistent with the PR's recording semantics but does not itself prove the delivered-notice contract.
  • Supplemental (all PASS): preview login + landing; delivery-defaults panel render + read-back; extensions registry (no channel installed); jobs page; logs page; end-to-end fallback-target journey above (create → fire → fail → record → settle → cleanup).

Hermetic Reborn integration suite (deterministic, offline, full product path)

Run on PR head f6cdf6158 with RUST_MIN_STACK=16MB (macOS debug thread-stack workaround). The only fake is the scripted model at the vendor-SDK seam — real product workflow, turn coordinator, scheduler, agent loop, ironclaw_llm decorator chain, and filesystem persistence all execute. No network, no credentials, no Docker.

  • reborn_integration_extension_delivery — 19 passed (2 case_*_postgres cases skipped: need Docker testcontainers; docker daemon not available in this environment — environmental, not PR-caused)
  • reborn_integration_triggered_submit — 17 passed (trigger-submitted run journeys)
  • reborn_group_triggers — 15 passed (trigger lifecycle/settlement group)
  • reborn_integration_delivery_user_journeys — 1 passed (delivery journey)
  • cargo test -p ironclaw_assistant --test run_delivery_contract — 33 passed (includes TriggeredRunDeliveryOutcomeKind::Delivered assertions for failed/recovery/cancelled/timeout branches — lines 2004/2060/2086/2127 — through the real DeliveryCoordinator with an in-process channel codec, plus sanitized-summary text assertions)
  • cargo test -p ironclaw_triggers — 132 lib + 52 integration passed
  • cargo check -p ironclaw_assistant — clean, no warnings

Total deterministic coverage: 269 tests passing on PR head (33 contract + 184 triggers + 52 hermetic integration), including the delivered-notice branch the live preview cannot run.

Regression result

Delivered-notice branch: not verifiable in preview (no channel extension/credentials). Recording + no-target branches: verified live against the fallback target — a failing triggered run was delivered-skipped with an explicit logged reason, recorded as Failed in run history (UI-visible), and settled by the poller with no replacement run. Remaining risk: actual outbound delivery (sanitized summary + triggered footer) to a real channel, covered by the deterministic contract suite above.

Skipped cases

  • Timeout branch: 30-minute max actionable wait makes it impractical in this session; needs a channel to be meaningful.
  • Chat-driven automation creation is nondeterministic (model-dependent); used once as the only supported way to create triggers — acceptable for a single clearly-named test automation, which was cleaned up.
  • Streaming/upload/responsive/auth recipes: not applicable — PR is backend-only (no WebUI/frontend files changed).

Cleanup

Test automation railway-qa-fail-test removed via chat; verified 0 automations remaining. No other test data created. Browser sessions closed.

@serrrfirat serrrfirat left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review (multi-agent)

Intent: Deliver sanitized terminal notices to the creator's preference target for triggered runs that fail, require recovery, are cancelled, or time out, instead of recording them silently as Skipped.

Shape: primary mode normal; no modifiers. 10 changed files (8 production, 1 test, 1 docs), 653 changed lines (628+/25-), diff source local-git (7a2e7bf...f6cdf615).

Coverage: complete — all 8 reviewers read all 3 packets through EOF (production-001, tests-001, docs-001), packet hashes verified against manifest, worktree at head f6cdf6158e7de5927699f54afddac664bcaabaec. Reviewers failed: none. Body-only: 0.

Stats: 10 findings (from 10 raw, 5 after dedup) across 2 files. Reviewers run: security, bugs, performance, tests, conventions, local-patterns, maintainability, approach.


Bugs (3)

  1. Low Timeout arm delivers timeout copy to runs mid-cancel/failure; terminal copy never sent (crates/product/ironclaw_assistant/src/run_delivery/triggered.rs:354-406, confidence 60) — anchor: triggered.rs:354
    The new RunWaitTimedOut arm fires for any run not terminal/actionable within max_wait, including CancelRequested or about-to-fail runs; it delivers DELIVERY_TIMEOUT_MESSAGE and the watcher exits, so the eventual Cancelled/Failed terminal copy is never sent. Small window, strictly better than silence, but the recorded Delivered outcome claims final delivery while the run is still non-terminal. Fix: short grace-period watch after the timeout notice (or suppress when the state is already CancelRequested).
  2. Low Cancelled-with-category arm unreachable in production; failure copy would mislabel a cancel (triggered.rs:800-831, confidence 55) — anchor: triggered.rs:800
    Cancelled runs carry no SanitizedFailure in the real system (failure is attached only to Failed/RecoveryRequired exits), so the Some(category) branch is exercised only by the scripted contract test. If a future cancel path attaches a failure, the delivered copy would mislabel an operator cancel as a failure and instruct retry. Fix: restrict to cancel-specific categories or drop the branch.
  3. Nit Final Ok(None) arm comment promises silence for blocked states the timeout arm actually notifies (triggered.rs:826-842, confidence 75) — anchor: triggered.rs:833
    wait_for_actionable_state never returns in-flight/blocked states to this function; they hit the timeout arm instead, so "stay silent / records Skipped" describes behavior the timeout arm overrides. Fix: comment as exhaustiveness-only.

Tests (1)

  1. Low Timeout-notice arm's Denied/Other outcome branches untested (triggered.rs:395-400, confidence 60) — anchor: triggered.rs:395
    Only Delivered and NoDefaultConfigured timeout outcomes are asserted. The Denied branch is reachable via the existing project-scoped denial fixture, and Other -> Failed via a failed delivery report. These determine the recorded outcome kind, the core observable #6896 changes. Fix: two new contract scenarios (..._records_denied, ..._records_failed).

Maintainability (2)

  1. Medium Timeout arm triplicates failure-to-outcome mapping and final-notice literal (triggered.rs:375-406, confidence 75) — anchor: triggered.rs:388
    The failure→outcome match now exists three times in deliver_triggered_run (timeout arm, OAuth-backstop arm, generic Err arm) and the final-reply literal six times; the timeout arm also hand-rebuilds the 7-field context the loop already constructs. Any future outcome-kind change must be applied in three places. Fix: extract deliver_terminal_notice + final_reply_notice helpers and route all arms through them (~35 lines deleted). (Also flagged by: bugs, tests, conventions)
  2. Low Failed/RecoveryRequired and Cancelled arms identical except fallback text (triggered.rs:780-833, confidence 55) — anchor: triggered.rs:780-833
    Two adjacent arms differing by one constant. Fix: merge into one arm with per-status fallback selection.

Conventions (2)

  1. Low deliver_triggered_run invariant doc now contradicts new timeout arm semantics (triggered.rs:273-278, confidence 60) — anchor: AGENTS.md "Update the owning contract/docs when behavior changes"
    Doc still claims the backstop is recorded as a failure signal; the new timeout arm records Delivered/NoDefaultConfigured/Denied and only delivery failure records Failed. Fix: update the invariant doc.
  2. Nit Timeout arm adds a third copy of the failure→outcome mapping match (triggered.rs:391-400, confidence 50) — anchor: triggered.rs:558-567, :589-596
    Duplicate of the OAuth-backstop and generic Err mappings. Fix: extract delivery_outcome_kind helper.

Local Patterns (1)

  1. Low Stale doc count: "Only three outputs" but five bullets in triggered surface contract (triggered.rs:611-621, confidence 90) — anchor: triggered.rs:611-621
    The diff added two terminal-output bullets without updating the lead-in count. Fix: drop the count or update to five.

Performance (1)

  1. Low Inline await of on_failed_fire_settled stalls active-cleanup sweep (crates/domains/ironclaw_triggers/src/worker/active_cleanup.rs:169-178, confidence 60) — anchor: active_cleanup.rs:169
    The new hook is awaited inline in the sweep loop; the sibling on_accepted_fire_settled path deliberately decouples via bounded spawn. Today's impls are cheap, but the trait is open — a future heavy observer stalls cleanup for the whole tick. Fix: bounded spawn (mirroring the sibling path) or document the cheap-observer contract.

Security (0) / Approach (0)

No findings. Security verified: static sanitized-summary table only (validated category, no injection path), scope+actor enforcement on target resolution, settlement fires only after durable clear, no production unwraps added. Approach: root cause of #6896 confirmed in base; PR reuses existing facilities (reborn_failure_summary_for_category, observer port, notification plumbing); all implementors enumerated; docs updated.

let mut delivered_blocked_marker: Option<BlockedActionableMarker> = None;
let mut messages_to_delete_after_final: Vec<DeliveredChannelMessage> = Vec::new();
// The trigger label is stable for the whole run; compute it once so the
// timeout-with-no-marker arm can build a terminal notice without waiting

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium — Timeout arm triplicates failure-to-outcome mapping and final-notice literal.

The new RunWaitTimedOut arm hand-builds a FinalReplyReady/TriggeredDelivery TriggeredNotification literal and repeats the deliver_triggered_notification failure->outcome match. That outcome match now exists three times in deliver_triggered_run (timeout arm, OAuth-backstop arm, generic Err arm) and the final-reply literal shape exists six times (new timeout, Failed/RecoveryRequired, Cancelled, plus pre-existing Completed, unserviceable-auth, OAuth-backstop arms). The timeout arm also hand-rebuilds the 7-field TriggeredNotificationContext that the loop body already constructs. A reader must diff the literals to confirm they are identical, and any future change to the mapping must be applied in three places.

Fix: Extract deliver_terminal_notice(services, ctx, text, trigger_label) -> TriggeredRunDeliveryOutcomeKind that builds the final notice (with a final_reply_notice(text, trigger_label, attachments, intent) constructor), delivers it, and maps TriggeredNotificationFailure to TriggeredRunDeliveryOutcomeKind once. Route the timeout arm, the OAuth-backstop arm, and the generic Err arm through it. Net deletion: ~35 lines of repeated match + literal.

Also flagged by: bugs/Low, tests/Low, conventions/Nit

authority: &authority,
};
let notice = TriggeredNotification {
event_kind: RunNotificationEventKind::FinalReplyReady,

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit — Final Ok(None) arm comment promises silence for blocked states the timeout arm actually notifies.

The trailing TurnStatus::Queued | Running | CancelRequested | BlockedResource | BlockedDependentRun | BlockedExternalTool => Ok(None) arm's comment says these states stay silent here and are surfaced through the WebUI, and Returning None records Skipped. But wait_for_actionable_state never returns any of these states to triggered_notification_for_state — it only returns terminal states or newly-blocked-actionable states. Non-actionable blocked and in-flight runs instead hit the RunWaitTimedOut arm and receive the timeout notice with a Delivered/NoDefaultConfigured/Denied/Failed outcome — never Skipped. The arm is compile-required exhaustiveness only; the comment describes behavior the timeout arm overrides.

Fix: Update the comment to state that these states never reach this function in the triggered loop (exhaustiveness only) and that they are handled by the timeout arm.

Also flagged by: bugs/Low, maintainability/Low

use async_trait::async_trait;
use chrono::Utc;
use ironclaw_extension_contracts::channel_adapter::OutboundPart;
use ironclaw_host_api::failure::summary::reborn_failure_summary_for_category;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low — deliver_triggered_run invariant doc now contradicts new timeout arm semantics.

Doc block on deliver_triggered_run still states the backstop is the failure signal ONLY for runs that never reached an actionable state at all, distinguished by delivered_blocked_marker. The new RunWaitTimedOut-without-marker arm now delivers the timeout notice and records Delivered/NoDefaultConfigured/Denied when the channel is reachable, so the backstop is no longer recorded as a failure signal for never-actionable runs; Failed is recorded only when the notice delivery itself fails. The invariant doc predates the change and was not updated.

Fix: Update the invariant doc block: the never-actionable backstop now delivers a terminal timeout notice and records the delivery outcome, not Failed.

/// - `BlockedAuth` → auth prompt (OAuth link) or, for non-OAuth, a
/// cancel + final-reply carrying the auth-unavailable notice
/// - `Completed` → final reply
/// - `Failed` / `RecoveryRequired` → final reply carrying the per-category

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low — Stale doc count: 'Only three outputs' but five bullets in triggered surface contract.

The diff extended the triggered_notification_for_state doc comment with two new terminal-output bullets (Failed/RecoveryRequired, Cancelled) but left the lead-in 'Only three outputs are minted here:' counting the pre-change three bullets (BlockedApproval, BlockedAuth, Completed). The comment now contradicts the code it documents; the staleness was introduced by this change.

Fix: Reword the lead-in so it cannot go stale with future arms: drop the count or update it to five.

fire_slot,
run_id,
})
.await;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low — Inline await of on_failed_fire_settled stalls active-cleanup sweep.

The new failure-settlement hook is awaited inline inside the per-record sweep loop, between the durable clear_active_fire and report/cursor advancement. Today's impls are cheap (production logs one warn; Noop does nothing; tests push to a Mutex), and the loop already awaits backend I/O per record, so marginal latency is negligible. But the trait is async and open (Send+Sync, any implementor); the sibling on_accepted_fire_settled path deliberately decouples via bounded spawn_post_submit_delivery + bounded buffer, while this path couples sweep latency and scan-cursor progress to observer behavior. A future heavy observer (e.g., network telemetry sink) would stall active-fire cleanup for the whole tick. No lock is held across the await, so no deadlock is introduced.

Fix: Detach the observer call with a bounded spawn (mirroring on_accepted_fire_settled), or harden the trait contract docs to require cheap non-blocking observers.

- Extract shared terminal-notice helpers (final_reply_notice,
  outcome_for_delivery_failure, deliver_terminal_notice) so the
  timeout, OAuth-backstop, and generic failure arms share one notice
  shape and outcome taxonomy instead of a third hand-rolled copy.
- Add a bounded race-grace window after the wait backstop: a run that
  crosses into a terminal state during the final wait (cancellation in
  flight, failure landing after the last poll) now delivers the correct
  terminal notice instead of the timeout copy.
- Cancelled runs always deliver the fixed cancellation notice; the
  failure-category branch was unreachable in production and would have
  mislabeled a host/operator cancel as a failure.
- Update the stale invariant doc, the five-output surface contract
  count, and the exhaustiveness-only comment on the non-actionable arm.
- Document the cheap/non-blocking contract on
  TriggerFireSettlementObserver (the worker awaits it inline in the
  poller sweep) and note it at the active-cleanup call site.
- Add contract coverage for the timeout arm's delivery-failure outcome
  (Failed) and a regression test proving the race-grace path delivers
  the cancellation notice; the cancelled-with-category test now asserts
  the cancellation notice wins.
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-7131 August 7, 2026 22:43 Destroyed
@github-actions github-actions Bot removed the size: L 200-499 changed lines label Aug 7, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/product/ironclaw_assistant/src/run_delivery/triggered.rs (1)

736-739: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Keep the grace poll within the configured deadline.

When grace_deadline is shorter than settings.poll_interval, this branch can sleep past the deadline and accept a terminal result that arrived after the intended race-window. Cap sleep() by grace_deadline - Instant::now(), and apply a per-call/remaining timeout to get_run_state before accepting that result. This also keeps crates/product/**/*.rs guarded behind product-mediated deadlines and timeouts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/product/ironclaw_assistant/src/run_delivery/triggered.rs` around lines
736 - 739, Update the grace-period polling loop around grace_deadline and
get_run_state to cap each sleep at the remaining duration until grace_deadline,
preventing poll_interval from overshooting the deadline. Apply the same per-call
remaining-time timeout to get_run_state, and only accept its terminal result
when the call completes within the configured grace window.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@crates/product/ironclaw_assistant/src/run_delivery/triggered.rs`:
- Around line 736-739: Update the grace-period polling loop around
grace_deadline and get_run_state to cap each sleep at the remaining duration
until grace_deadline, preventing poll_interval from overshooting the deadline.
Apply the same per-call remaining-time timeout to get_run_state, and only accept
its terminal result when the call completes within the configured grace window.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a77ecda4-786a-4df5-b7a1-218d3a81163e

📥 Commits

Reviewing files that changed from the base of the PR and between 3452f43 and 060d70f.

📒 Files selected for processing (5)
  • crates/app/ironclaw_architecture_tests/tests/reborn_restructure_baselines.rs
  • crates/product/ironclaw_assistant/src/run_delivery/prompts.rs
  • crates/product/ironclaw_assistant/src/run_delivery/triggered.rs
  • crates/product/ironclaw_assistant/tests/run_delivery_contract.rs
  • scripts/ci/composition-budget.toml

# Conflicts:
#	crates/app/ironclaw_architecture_tests/tests/reborn_restructure_baselines.rs
#	scripts/ci/composition-budget.toml
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / pr-a7ad01-7131 August 10, 2026 10:24 Destroyed
BenKurrek
BenKurrek previously approved these changes Aug 10, 2026
@serrrfirat
serrrfirat added this pull request to the merge queue Aug 10, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Aug 10, 2026
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / pr-a7ad01-7131 August 10, 2026 12:34 Destroyed
@serrrfirat
serrrfirat added this pull request to the merge queue Aug 10, 2026
Merged via the queue into main with commit 38f8de4 Aug 10, 2026
45 checks passed
@serrrfirat
serrrfirat deleted the implement-issue-6896-fix branch August 10, 2026 14:10
BenKurrek added a commit that referenced this pull request Aug 10, 2026
…iring race test

The unbound-Telegram pairing race test (merged from main via #7131) matched the
working indicator by the substring "is thinking", which the varied copy
removed. Identify it structurally instead — the race-chat message anchored to
618 that is not the final reply — so it survives the copy change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01USZCuPuTtrrhDc8XnqnapQ
personal-upstream-sync Bot pushed a commit to theredspoon/ironclaw that referenced this pull request Aug 10, 2026
…rogress nudges (nearai#7446)

* feat(channels): vary the "working" notice per run

Replace the single "Ironclaw is thinking..." working indicator with a small
rotation of warm notices ("On it!", "Let me look into that…", …), picked
deterministically per run (by run id) so a shared channel with several
concurrent runs does not fill with identical lines while one run keeps a
single voice.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01USZCuPuTtrrhDc8XnqnapQ

* test(channels): assert working-indicator structure, not its varied copy

The varied per-run working notice broke three suites that pinned the exact
"Ironclaw is thinking..." literal. The literal is volatile content now;
pin the copy in one place (the prompts unit test) and assert *structure*
everywhere else — a distinct working indicator is posted, then retracted,
then the reply.

- run_delivery_contract.rs: a non-empty working notice distinct from the
  final reply precedes it (3 sites).
- e2e_tests.rs (extension_host): the running turn posts a non-empty
  working indicator (2 sites) + generalized a stale doc comment.
- extension_delivery.rs (root integration): select the working sendMessage
  by the call, not the words — the model is paused so it is the only
  /sendMessage before release.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01USZCuPuTtrrhDc8XnqnapQ

* feat(channels): rich working indicator — reactions, failure states, progress nudges

Batches the full shared-channel working-indicator UX onto the varied-copy PR:

- Reactions on the triggering message track the run: 👀 working → ✅ done,
  → ⚠️ when parked on an approval/auth prompt (and back to 👀 on resume),
  → ❌ on failure/timeout. New OutboundPart::React + neutral RunReaction /
  ReactionAction in ironclaw_extension_contracts; Slack reactions.add/remove,
  Telegram setMessageReaction (allowlist-mapped), egress updated in both
  manifests; web-push reports unsupported; DeliveryIntent::Reaction (notice-class).
- Failure/timeout states now reach the channel: a terminal failed/cancelled run
  retracts the stuck "thinking" indicator and posts a brief, diagnostic-free
  failure notice (source-routed through the same reliable path) instead of going
  silent; timeouts retract the indicator too.
- Progress nudges: a long run refreshes its indicator in place with escalating
  "still working" copy — first at 30s, then each gap doubling — so it never
  looks stalled.

The source message's vendor ref already rides ExternalConversationRef, so no new
ingress plumbing was needed. The reaction lifecycle is a small state machine
(set_source_reaction) in the delivery observer.

Also addresses prior review: auth-flow assertions now require the working
indicator distinct from the auth prompt too (CodeRabbit); reaction / failure /
needs-input / nudge lifecycles are covered through observe_ack at the adapter
seam (caller-level coverage).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01USZCuPuTtrrhDc8XnqnapQ

* fix(channels): keep neutral code vendor-agnostic in reaction comments

The reaction doc/comments named Telegram and Slack in the neutral channel
contract and the delivery observer, tripping the
reborn_generic_code_names_no_concrete_extension architecture gate. Reword to
generic phrasing (vendor reaction APIs / the originating channel).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01USZCuPuTtrrhDc8XnqnapQ

* test(channels): identify the working indicator structurally in the pairing race test

The unbound-Telegram pairing race test (merged from main via nearai#7131) matched the
working indicator by the substring "is thinking", which the varied copy
removed. Identify it structurally instead — the race-chat message anchored to
618 that is not the final reply — so it survives the copy change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01USZCuPuTtrrhDc8XnqnapQ

* test(channels): admit the new reaction egress paths in the manifest allowlist

The reaction feature added reactions.add/remove (Slack) and setMessageReaction
(Telegram) to the channel egress allowlists; the first-party manifest parity
tests pin those lists exactly (a security boundary — a new egress path must be
a reviewed change), so add the new paths to the expected sets.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01USZCuPuTtrrhDc8XnqnapQ

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
l3ocifer pushed a commit to l3ocifer/frick-ironclaw that referenced this pull request Sep 3, 2026
…rai#6896) (nearai#7131)

* fix(run_delivery): deliver triggered run failures to the creator (nearai#6896)

Scheduled/triggered runs that ended in Failed, Cancelled, or
RecoveryRequired produced no user-visible notification: the triggered
delivery driver minted notifications only for Completed /
BlockedApproval / BlockedAuth and recorded every other terminal status
as Skipped. A run that timed out before reaching an actionable state
only logged a warn and recorded Failed, leaving the creator in silence.

Delivery:
- triggered_notification_for_state now mints a FinalReplyReady
  notification for Failed and RecoveryRequired using the existing
  per-category failure summaries (reborn_failure_summary_for_category)
  over state.failure.category(), with a generic fallback when no
  category is present.
- Cancelled mints the same notification, preferring a failure-category
  summary when one is present and falling back to a fixed cancellation
  notice otherwise.
- The RunWaitTimedOut branch with no prior blocked marker now delivers
  the timeout notice as a terminal reply instead of recording Failed.
- The wildcard arm is replaced with explicit non-actionable statuses
  (Queued, Running, CancelRequested, BlockedResource,
  BlockedDependentRun, BlockedExternalTool) so a future status fails to
  compile rather than silently skipping.

Observer:
- TriggerFireSettlementObserver gains on_failed_fire_settled as a
  default no-op method, plus a TriggerFailedFireSettlement event
  carrying tenant/trigger/fire-slot/run-id/history-status. Noop and
  existing implementors keep compiling.
- The active-cleanup sweep fires on_failed_fire_settled when
  clear_active_fire succeeds with TriggerRunHistoryStatus::Error, so
  post-accept failures are observable for automation health. Ok,
  Running, and already-cleared fires do not fire the hook.

Tests:
- run_delivery_contract: Failed+model_error, Failed without category,
  Cancelled, and timeout-before-actionable all assert a Delivered
  outcome with the expected notice text and footer.
- worker tests: a terminal-Error active fire fires exactly one
  on_failed_fire_settled; a terminal-Ok active fire fires none.

The larger retry/redrive budget for failed post-accept fires
(retry_disposition has zero production callers) is intentionally left
for a follow-up; it is out of scope for this surgical delivery fix.

* style: cargo fmt the nearai#6896 delivery fix

* fix(triggers): address terminal delivery review feedback

* fix(assistant): drop unused UserId import after merge

* fix(run_delivery): address multi-agent review findings

- Extract shared terminal-notice helpers (final_reply_notice,
  outcome_for_delivery_failure, deliver_terminal_notice) so the
  timeout, OAuth-backstop, and generic failure arms share one notice
  shape and outcome taxonomy instead of a third hand-rolled copy.
- Add a bounded race-grace window after the wait backstop: a run that
  crosses into a terminal state during the final wait (cancellation in
  flight, failure landing after the last poll) now delivers the correct
  terminal notice instead of the timeout copy.
- Cancelled runs always deliver the fixed cancellation notice; the
  failure-category branch was unreachable in production and would have
  mislabeled a host/operator cancel as a failure.
- Update the stale invariant doc, the five-output surface contract
  count, and the exhaustiveness-only comment on the non-actionable arm.
- Document the cheap/non-blocking contract on
  TriggerFireSettlementObserver (the worker awaits it inline in the
  poller sweep) and note it at the active-cleanup call site.
- Add contract coverage for the timeout arm's delivery-failure outcome
  (Failed) and a regression test proving the race-grace path delivers
  the cancellation notice; the cancelled-with-category test now asserts
  the cancellation notice wins.

* fix(run_delivery): address review comments and restore CI gates

Review fixes (CodeRabbit on 01e887f/f8af109):
- Grace loop fails loud: log the bound TurnError on state-poll failure and
  the RunDeliveryError on terminal-notice build failure before falling back
  to the timeout copy, with silent-ok markers on both intentional fallbacks.
- Hoist TriggeredReplyTargetAuthority, CodecChannelTargetResolver, and
  TriggeredNotificationContext to one construction before the watcher loop;
  the race-grace arm, timeout arm, and loop body now share it.
- Collapse the duplicated failure-summary expression into one closure and
  name TurnStatus::Failed explicitly so future statuses are compiler-visible.
- Drop the stale "Only three states" count from the surface-contract doc.
- Test fixture: encode the late-terminal flip as one Option<(usize,
  ScriptedRunState)> field instead of two correlated Options with an expect.
- Terminal-crossing test: document why flip_after=30 deterministically
  outruns the wait poll budget and assert the grace loop issues no
  cancellation (cancel_calls == 0).

CI:
- composition-budget: re-seed loc_ceiling 40432 -> 40593 (measured on the
  merged tree; the nearai#7131 settlement observer adds +161 governed LOC of
  wiring) and move the arch-test record with it.
- trigger_poller: use the colon-form tracing target required by nearai#7146.

* ci: re-trigger pull_request workflows for c2460ed

* fix(composition): capture the settlement health warn in the observer test

The traced_test default filter is {crate}=trace, which drops events whose
metadata target is `ironclaw::reborn::…`. The observer warning is emitted
with the colon-form target (required by nearai#7146 — the equals form recorded a
field and never matched RUST_LOG target filters), so the test saw an empty
buffer. Enable tracing-test's no-env-filter feature, the same pattern the
capabilities/host-runtime/mcp/loop crates use for cross-target assertions.

Re-seed the composition budget to the merged-tree measurement (40747 ->
40867): nearai#7131's observer wiring lands on top of post-measurement mainline
inflow; measured with the gate, set to current. The arch-test record moves
with the manifest.

* fix(run_delivery): merge main and adapt to notice_discriminator String

- Merge origin/main (nearai#7377 run-acts-as-invoker, nearai#7323, nearai#7382, nearai#6938,
  nearai#7280, nearai#7393, nearai#7389, nearai#7364, nearai#7228, nearai#7371, nearai#7399).
- main's nearai#7377 landed a narrower terminal arm (generic failure notice for
  TurnStatus::Failed only); keep the nearai#6896 arm, which covers Failed and
  RecoveryRequired with sanitized per-category summaries plus Cancelled
  and the timeout grace path, and adapt to the Option<String>
  notice_discriminator main introduced.
- Re-seed the composition budget to the merged-tree measurement
  (40811 -> 40861, the run-failure settlement observer lands +50 governed
  LOC); the arch-test record moves with the manifest.
l3ocifer pushed a commit to l3ocifer/frick-ironclaw that referenced this pull request Sep 3, 2026
…rogress nudges (nearai#7446)

* feat(channels): vary the "working" notice per run

Replace the single "Ironclaw is thinking..." working indicator with a small
rotation of warm notices ("On it!", "Let me look into that…", …), picked
deterministically per run (by run id) so a shared channel with several
concurrent runs does not fill with identical lines while one run keeps a
single voice.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01USZCuPuTtrrhDc8XnqnapQ

* test(channels): assert working-indicator structure, not its varied copy

The varied per-run working notice broke three suites that pinned the exact
"Ironclaw is thinking..." literal. The literal is volatile content now;
pin the copy in one place (the prompts unit test) and assert *structure*
everywhere else — a distinct working indicator is posted, then retracted,
then the reply.

- run_delivery_contract.rs: a non-empty working notice distinct from the
  final reply precedes it (3 sites).
- e2e_tests.rs (extension_host): the running turn posts a non-empty
  working indicator (2 sites) + generalized a stale doc comment.
- extension_delivery.rs (root integration): select the working sendMessage
  by the call, not the words — the model is paused so it is the only
  /sendMessage before release.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01USZCuPuTtrrhDc8XnqnapQ

* feat(channels): rich working indicator — reactions, failure states, progress nudges

Batches the full shared-channel working-indicator UX onto the varied-copy PR:

- Reactions on the triggering message track the run: 👀 working → ✅ done,
  → ⚠️ when parked on an approval/auth prompt (and back to 👀 on resume),
  → ❌ on failure/timeout. New OutboundPart::React + neutral RunReaction /
  ReactionAction in ironclaw_extension_contracts; Slack reactions.add/remove,
  Telegram setMessageReaction (allowlist-mapped), egress updated in both
  manifests; web-push reports unsupported; DeliveryIntent::Reaction (notice-class).
- Failure/timeout states now reach the channel: a terminal failed/cancelled run
  retracts the stuck "thinking" indicator and posts a brief, diagnostic-free
  failure notice (source-routed through the same reliable path) instead of going
  silent; timeouts retract the indicator too.
- Progress nudges: a long run refreshes its indicator in place with escalating
  "still working" copy — first at 30s, then each gap doubling — so it never
  looks stalled.

The source message's vendor ref already rides ExternalConversationRef, so no new
ingress plumbing was needed. The reaction lifecycle is a small state machine
(set_source_reaction) in the delivery observer.

Also addresses prior review: auth-flow assertions now require the working
indicator distinct from the auth prompt too (CodeRabbit); reaction / failure /
needs-input / nudge lifecycles are covered through observe_ack at the adapter
seam (caller-level coverage).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01USZCuPuTtrrhDc8XnqnapQ

* fix(channels): keep neutral code vendor-agnostic in reaction comments

The reaction doc/comments named Telegram and Slack in the neutral channel
contract and the delivery observer, tripping the
reborn_generic_code_names_no_concrete_extension architecture gate. Reword to
generic phrasing (vendor reaction APIs / the originating channel).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01USZCuPuTtrrhDc8XnqnapQ

* test(channels): identify the working indicator structurally in the pairing race test

The unbound-Telegram pairing race test (merged from main via nearai#7131) matched the
working indicator by the substring "is thinking", which the varied copy
removed. Identify it structurally instead — the race-chat message anchored to
618 that is not the final reply — so it survives the copy change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01USZCuPuTtrrhDc8XnqnapQ

* test(channels): admit the new reaction egress paths in the manifest allowlist

The reaction feature added reactions.add/remove (Slack) and setMessageReaction
(Telegram) to the channel egress allowlists; the first-party manifest parity
tests pin those lists exactly (a security boundary — a new egress path must be
a reviewed change), so add the new paths to the expected sets.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01USZCuPuTtrrhDc8XnqnapQ

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

This branch was successfully deployed

No deployments
ironclaw-ci-preview / pr-a7ad01-7131 — 767f95db Deployed Aug 10, 2026 by railway-app[bot]
ironclaw-ci-preview / ironclaw-pr-7131 — d8f5bcc3 Deployed Aug 8, 2026 by railway-app[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor: core 20+ merged PRs risk: low Changes to docs, tests, or low-risk modules scope: docs Documentation size: XL 500+ changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Scheduled/triggered run failures are never delivered to the user Routine fails with generic error messages instead of reporting root cause

2 participants